/**
 *
 *  Copyright (c) 2011, AstroDAbis
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without modification,
 *  are permitted provided that the following conditions are met:
 *
 *      * Redistributions of source code must retain the above copyright notice,
 *        this list of conditions and the following disclaimer.
 *      * Redistributions in binary form must reproduce the above copyright notice,
 *        this list of conditions and the following disclaimer in the documentation
 *        and/or other materials provided with the distribution.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 *  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 *  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 *  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 *  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 *  OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */
package org.jiscinvolve.astrodabis.mallow.impl.model.api;

import java.net.URL;

import java.util.List;

import javax.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Transient;

import javax.persistence.ManyToOne;
import javax.persistence.JoinColumn;
import javax.persistence.Enumerated;

import static javax.persistence.EnumType.STRING;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.transaction.annotation.Transactional; 

import org.springframework.jndi.JndiTemplate;


import org.jiscinvolve.astrodabis.galium.activity.SimpleQueryParam;
import org.jiscinvolve.astrodabis.galium.activity.SimpleQueryParamImpl;
import org.jiscinvolve.astrodabis.galium.activity.SimpleQueryActivity;
import org.jiscinvolve.astrodabis.galium.activity.SimpleQueryActivityImpl;

import uk.org.ogsadai.resource.request.RequestStatus;
import uk.org.ogsadai.resource.request.RequestExecutionStatus;

import org.jiscinvolve.astrodabis.mallow.model.api.Tag;
import org.jiscinvolve.astrodabis.mallow.model.api.Query;
import org.jiscinvolve.astrodabis.mallow.model.api.QueryBean;
import org.jiscinvolve.astrodabis.mallow.model.api.QueryTag;
import org.jiscinvolve.astrodabis.mallow.model.api.QueryTagManager;
import org.jiscinvolve.astrodabis.mallow.model.api.Account;
import org.jiscinvolve.astrodabis.mallow.model.api.Result;
import org.jiscinvolve.astrodabis.mallow.model.api.ResultManager;

import org.jiscinvolve.astrodabis.mallow.model.tap.TapTable;
import org.jiscinvolve.astrodabis.mallow.model.tap.TapService;

import org.jiscinvolve.astrodabis.mallow.util.GenericIterable;
import org.jiscinvolve.astrodabis.mallow.impl.model.api.AccountImpl;
import org.jiscinvolve.astrodabis.mallow.impl.model.jpa.ManagedObjectImpl;

import org.jiscinvolve.astrodabis.mallow.impl.model.tap.TapTableImpl;
import org.jiscinvolve.astrodabis.mallow.impl.model.tap.TapServiceImpl;

/**
 * Implementation of the Query bean.
 *
 */
@Entity
@Table(name="queries")
public class QueryImpl
extends ManagedObjectImpl
implements Query
    {

    /**
     * Our debug logger.
     * 
     */
    private static Log log = LogFactory.getLog(QueryImpl.class);

    /**
     * Delay before running a query.
     *
     */
    public static final long QUERY_DELAY = 5000 ;

    /**
     * Our shared JndiTemplate.
     *
     */
    protected static JndiTemplate jndi = new JndiTemplate();

    /**
     * Protected constructor.
     *
     */
    protected QueryImpl()
        {
        super();
        log.debug("QueryImpl()");
        }

    /**
     * Public constructor.
     *
     */
    public QueryImpl(AccountImpl owner, TapTableImpl table, QueryBean bean)
        {
        super(owner);
        log.debug("QueryImpl(AccountImpl, TapTableImpl, QueryBean)");
        this.table  = table;
        this.status = Status.EDITING;
        //
        // Set the text properties.
        this.name   = bean.getName();
        this.text   = bean.getText();
        this.where  = bean.getWhere();
        //
        // Build the ADQL query.
        this.query = this.adql();

        }

    /**
     * Reference to 'this' used by inner classes.
     *
     */
    protected QueryImpl self()
        {
        return this ;
        }

    /**
     * The Query name (optional).
     *
     */
    @Column(name="name")
    public String name;

    @Override
    public String name()
        {
        return this.name ;
        }

    /**
     * The Query description (optional).
     *
     */
    @Column(name="text")
    public String text;

    @Override
    public String text()
        {
        return this.text ;
        }

    /**
     * The Query table.
     *
     */
    @ManyToOne
    @JoinColumn(name="taptable", nullable=false)
    private TapTableImpl table;

    @Override
    public TapTableImpl table()
        {
        return this.table ;
        }

    @Override
    public TapServiceImpl service()
        {
        return this.table.service() ;
        }

    /**
     * The Query status.
     * http://www.java2s.com/Code/Java/JPA/MapJavaEnumType.htm
     *
     */
    @Column(name="status")
    @Enumerated(STRING)
    private Status status;

    @Override
    public Status status()
        {
        return this.status ;
        }

    /**
     * Set the Query status (used by ManagerImpl).
     *
     */
    public void status(Status value)
        {
        log.debug("QueryImpl.status()");
        log.debug(" Status [" + value + "]");
        this.status = value ;
        }

    /**
     * Run the query
     *
     */
    public void run()
        {
        log.debug("QueryImpl.run()");
        log.debug("  Ident [" + this.ident() + "]");
        manager().run(
            this
            );
        }

    /**
     * Cancel the query
     *
     */
    public void cancel()
        {
        log.debug("QueryImpl.cancel()");
        log.debug("  Ident [" + this.ident() + "]");
/*
        manager().cancel(
            this
            );
 */
        }

    /**
     * Mark the query as running.
     *
     */
    public void running()
        {
        log.debug("QueryImpl.running()");
        log.debug("  Ident [" + this.ident() + "]");
        manager().running(
            this
            );
        }

    /**
     * Mark the query as completed.
     *
     */
    public void completed()
        {
        log.debug("QueryImpl.completed()");
        log.debug("  Ident [" + this.ident() + "]");
        manager().completed(
            this
            );
        }

    /**
     * Mark the query as failed.
     *
     */
    public void failed()
        {
        log.debug("QueryImpl.failed()");
        log.debug("  Ident [" + this.ident() + "]");
        manager().failed(
            this
            );
        }

    /**
     * Mark the query as cancelled.
     *
     */
    public void cancelled()
        {
        log.debug("QueryImpl.cancelled()");
        log.debug("  Ident [" + this.ident() + "]");
        manager().cancelled(
            this
            );
        }

    /**
     * Run the query.
     *
     */
    public void frog()
        {
        log.debug("QueryImpl.execute()");
        log.debug("  Ident [" + this.ident() + "]");

        log.debug("Query.sleeping");
        try {
            Thread.sleep(QUERY_DELAY);
            }
        catch (Exception ouch)
            {
            log.error("Query.interupted");
            }
        //
        // Mark our query as running.
        try {
            this.running();
            }
        catch (Exception ouch)
            {
            log.error("Failed to update query status [" + this.ident() + "][" + ouch.getClass() + "][" + ouch.getMessage() + "]");
            return ;
            }
        //
        // Create our OGSA-DAI Activity.
        SimpleQueryActivity activity = null ;
        try {
//
// OGSA-DAI service params.
            //String endpoint  = "http://beta.virtual.metagrid.co.uk:8080/dai/services/" ;
            //String endpoint  = "http://astro.metagrid.co.uk:8080/ogsadai/services/" ;
//
// Use simple JNDI properties.
// http://www.javabeat.net/articles/47-accessing-objects-in-jndi-using-spring-4.html
            String endpoint    = (String) jndi.lookup("java:comp/env/astrodabis/ogsadai/endpoint");
            String resultdb    = (String) jndi.lookup("java:comp/env/astrodabis/ogsadai/results/resource");
            String resulttable = (String) jndi.lookup("java:comp/env/astrodabis/ogsadai/results/table");

//
// TAP service params.
//String resource   = "ROEukidssDR5" ;
//String tablename  = "gcsSource" ;
//String tablealias = "source" ;
//String tablekey   = "sourceID" ;

String resource   = this.table().service().name();

            activity = new SimpleQueryActivityImpl(
                new SimpleQueryParamImpl(
                    new URL(endpoint),
                    resultdb,
                    resulttable,
                    resource,
                    this.ident(),
                    this.adql()
                    )
                );
            }
        catch (Throwable ouch)
            {
            log.error("Failed to create activity [" + this.ident() + "][" + ouch.getClass() + "][" + ouch.getMessage() + "]");
            try {
                this.failed();
                }
            catch (Throwable waah)
                {
                log.error("Failed to update status [" + this.ident() + "][" + waah.getClass() + "][" + waah.getMessage() + "]");
                }
            return ;
            }
        //
        // Run our activity.
        try {
            activity.execute();
            }
        catch (Throwable ouch)
            {
            log.warn("Failed to execute activity [" + this.ident() + "][" + ouch.getClass() + "][" + ouch.getMessage() + "]");
            try {
                this.failed();
                }
            catch (Throwable waah)
                {
                log.error("Failed to update status [" + this.ident() + "][" + waah.getClass() + "][" + waah.getMessage() + "]");
                }
            return ;
            }
        //
        // Check the activity status.
        try {
            switch(activity.status())
                {
                case PENDING :
                case RUNNING :
                    break ;

                case COMPLETED :
                    this.completed();
                    break ;

                case FAILED :
                    this.failed();
                    break ;

                case CANCELLED :
                    this.cancelled();
                    break ;

                default :
                    break ;
                }
            }
        catch (Throwable ouch)
            {
            log.error("Failed to update status [" + this.ident() + "][" + ouch.getClass() + "][" + ouch.getMessage() + "]");
            return ;
            }
        }    

    @Override
    public ResultManager results()
        {
        return new ResultManager()
            {
            @Override
            @Transactional(readOnly = true)
            public Result select(Long ident)
                {
                return manager().results().select(
                    ident
                    );
                }

            @Override
            @Transactional(readOnly = true)
            public Iterable<Result> select()
                {
                return new GenericIterable<Result, ResultImpl>(
                    manager().results(
                        self()
                        )
                    );
                }
            };
        }

    @Override
    public QueryTagManager tags()
        {
        return new QueryTagManager()
            {
            @Override
            @Transactional(readOnly = true)
            public QueryTag select(String ident)
                {
                List<QueryTag> list = manager().template().find(
                    "FROM QueryTagImpl WHERE query = ? AND ident = ?",
                    self(),
                    ident
                    );  
                if (list.isEmpty())
                    {
                    return null ;
                    }
                else {
                    return list.get(0);
                    }
                }

            @Override
            @Transactional(readOnly = true)
            public Iterable<QueryTag> select()
                {
                return new GenericIterable<QueryTag, QueryTagImpl>(
                    manager().template().find(
                        "FROM QueryTagImpl WHERE query = ? ORDER BY created DESC",
                        self()
                        )  
                    );
                }
            };
        }

    @Override
    public void update(QueryBean bean)
        {
        this.update(
            bean,
            true
            );
        }

    @Override
    public void update(QueryBean bean, boolean save)
        {
        //
        // Update the text properties.
        this.name = bean.getName();
        this.text = bean.getText();
        //
        // If the query hasn't been run yet.
        if (this.status() == Status.EDITING)
            {
            //
            // Update the ADQL properties.
            this.where = bean.getWhere();
            //
            // Re-build the ADQL query.
            this.query = this.adql();
            }
        //
        // Save the object.
        if (save)
            {
            this.save();
            }
        }

    /**
     * The ADQL where clause.
     *
     */
    @Column(name="adql")
    private String where; 

    @Override
    public String where()
        {
        return this.where ;
        }

    /**
     * The full ADQL query.
     *
     */
    @Column(name="query")
    private String query; 

    @Override
    public String query()
        {
        return this.query ;
        }

    @Override
    public String head()
        {
        StringBuilder builder = new StringBuilder();
        builder.append("SELECT");
        builder.append(" ");
        builder.append(this.table().name());
        builder.append(".");
        builder.append(this.table().key());

        builder.append(" ");
        builder.append("AS");

        builder.append(" ");
        builder.append("object");

        builder.append(" ");
        builder.append("FROM");
        builder.append(" ");
        builder.append(this.table().name());

        builder.append(" ");
        builder.append("WHERE");

        return builder.toString();
        }

    @Override
    public String tail()
        {
        StringBuilder builder = new StringBuilder();

        return builder.toString();
        }

    /**
     * Build the full ADQL query.
     *
     */
    public String adql()
        {
/*
        String tablename  = this.table().name() ;
        String tablekey   = this.table().key()  ;

        //String statement = "Select a.sourceID as object, a.ra as weight From gcsSource as a Where a.ra Between '56.28' And '56.30' And a.dec Between '50.60' And '50.62'";
        //String statement = "Select sourceID as object, ra as weight From gcsSource Where ra Between '56.28' And '56.30' And dec Between '50.60' And '50.62'";
        String template  = "SELECT gcsSource.sourceID AS object, gcsSource.ra AS weight FROM gcsSource WHERE {where}";
        String statement = template.replace(
            "{where}",
            this.where()
            );
 */

        StringBuilder builder = new StringBuilder();

        builder.append(
            this.head()
            );

        builder.append(" ");
        builder.append(this.where());

        builder.append(" ");
        builder.append(
            this.tail()
            );

        return builder.toString() ;

        }

    }


